Skip to content

feat(claude-code): use Nix-packaged pi bridge#243

Merged
rrvsh merged 6 commits into
primefrom
pi-claude-bridge-nix-integration
Jun 29, 2026
Merged

feat(claude-code): use Nix-packaged pi bridge#243
rrvsh merged 6 commits into
primefrom
pi-claude-bridge-nix-integration

Conversation

@rrvsh

@rrvsh rrvsh commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Summary

This PR switches tools from the npm-installed Claude bridge to the Nix-packaged bridge from rrvsh/pi-claude-bridge-nix. The package is consumed as a Pi local package path from the Nix store:

  • Adds the pi-claude-bridge flake input in flake.nix.
  • Resolves the package for the host system and passes bridge.passthru.packagePath into programs.pi-coding-agent.settings.packages in nix/modules/claude-code.nix.
  • Locks the bridge flake to 332eb2b, which contains the confirmed runtime fixes and Cachix publication workflow.

Investigation summary

The original symptom on auto was that Claude bridge sessions often stalled after MCP/tool calls. The user-visible pattern was:

  1. Claude requested an MCP or Bash tool call.
  2. One tool result reached Claude.
  3. The bridge timed out after 90s with:
    • Claude Code stream idle timeout after 90s
    • delivered 1/2, resolved 1/2, waiting=1
  4. The session rebuilt or rotated.
  5. Typing go or keep going often caused Claude to repeat the same MCP call until the missing result finally landed.

Evidence from /Users/binmohm/.pi/agent/claude-bridge-diag.log and the referenced Claude/Pi JSONL sessions showed repeated tool_result_delivery_mismatch events with expectedCount: 2, deliveredCount: 1, resolvedCount: 1, and waitingCount: 1. A recent concrete event showed Claude emitted two tool uses, Pi received and returned the first result, and the second expected tool id remained waiting until the idle timeout tore down the query.

A separate build investigation found that auto could not reach registry.npmjs.org; prefetch-npm-deps connections stayed in SYN_SENT. That is why the bridge package also needs CI/Cachix publication rather than relying on local npm dependency fetching on auto.

Confirmed root causes

1. Active result delivery trusted only tail-extracted tool results

The bridge active-query path previously extracted tool results from the current tail and delivered those to pending MCP handlers. That missed cases where Pi history contained the result, but the result was not in the tail returned by extractAllToolResults(context) because of interleaved messages or assistant boundaries.

The Nix package patch now routes active result delivery through deliverToolResults() in patches/fix-multi-tool-results.patch. The helper scans full Pi context for recorded waiting ids, supplements results that are present-but-not-tail-extracted, rejects unknown ids, and skips duplicate resolved ids.

Why this fix was chosen:

  • It preserves the safety invariant that only recorded Claude tool-call ids may receive results.
  • It distinguishes presentButNotExtractedIds from missingFromContextIds, so future incidents reveal whether the result existed in Pi history or was genuinely absent.
  • It keeps hard failure behavior for unknown result ids instead of guessing.

Alternative considered:

  • Only add/adjust transcript conversion tests. This was rejected because conversion is not active delivery; the live issue was pending MCP handlers not receiving results during the running query.

2. The stream-idle watchdog fired while Claude was legitimately waiting on tools

The watchdog treated “no assistant/tool output for 90s” as a retryable Claude stream stall. That is wrong while an MCP handler is still pending; Claude Code is expected to be quiet until it receives the tool result.

The bridge patch adds pending tool state to the watchdog state and suppresses monitoring while pendingToolCallCount > 0:

Why this fix was chosen:

  • It keeps the watchdog for genuine pre-output Claude stalls.
  • It prevents the bridge from aborting a valid long-running tool wait.
  • It does not hide real missing-result mismatches; teardown diagnostics still fire if the query actually ends with unresolved tool ids.

Alternatives considered:

  • Disable CLAUDE_BRIDGE_STREAM_IDLE_TIMEOUT entirely. Rejected because it would remove protection against genuine Claude stream stalls.
  • Increase the timeout globally. Rejected because long tools can exceed any arbitrary global timeout, and short genuine stalls would take longer to detect.
  • Treat every idle as rate limit and retry. Rejected because it caused the observed repeated MCP-call loop.

3. Claude could emit a second tool_use while Pi stream was closed, so Pi never saw it

The live test on auto exposed a second root cause. Claude Code emitted another assistant tool_use while the Pi stream was already closed for a prior tool-use boundary. The bridge recorded/claimed that id internally, but the tool call was never surfaced to Pi, so Pi could not execute it and produce a result.

The patch now tracks those calls as undelivered and flushes them back to Pi:

Why this fix was chosen:

  • It preserves id-based matching. The bridge does not invent a result or map the result to another handler.
  • It makes Pi execute the exact tool call Claude emitted, which is the only safe way to get a real tool result.
  • It fixes the repeated keep going loop by making the missing call visible to Pi during the same active query.

Alternatives considered:

  • Return a synthetic error result to Claude for hidden/undelivered calls. Rejected because it would preserve transcript shape but lose the real tool output.
  • Re-run the whole Claude query after teardown. Rejected because that is the failure mode users were already seeing.
  • Deliver a placeholder and rely on continuation. Rejected because it causes repeated MCP calls and makes the conversation state less faithful.

Packaging and Cachix remediation

auto is a restricted host that cannot reliably reach registry.npmjs.org. The bridge package uses buildNpmPackage, whose fixed-output dependency derivation runs prefetch-npm-deps against tarball URLs from package-lock.json. On auto, those TCP connections timed out.

The bridge repository now publishes both the npm dependency derivation and the final bridge package to Cachix:

  • Build workflow authenticates to rrvsh Cachix with CACHIX_AUTH_TOKEN and builds both pi-claude-bridge.npmDeps and pi-claude-bridge for x86_64-linux and aarch64-darwin: build.yml.
  • The package derivation applies the runtime patch before rebuilding the bundle and runs focused tests in checkPhase: nix/pi-claude-bridge.nix.

Why this was chosen:

  • It keeps the upstream lockfile and npm integrity model intact.
  • It avoids relying on a third-party npm mirror for normal host rebuilds.
  • It lets auto substitute from rrvsh.cachix.org instead of contacting npm.

Alternatives considered:

  • Rewrite lockfile URLs to registry.npmmirror.com. This worked as a proof of remediation, but it changes the registry trust boundary and is better as a fallback than the default path.
  • Vendor npm deps into the repository. Rejected as heavier and noisier than Cachix.
  • Patch bundled JS from the npm tarball. Rejected because the runtime fix belongs in source and should be rebuilt/tested.
  • Rely on local npm network access on auto. Rejected because investigation proved direct npm registry access is blocked or timing out.

Validation

Bridge package validation:

  • nix build --print-build-logs .#pi-claude-bridge passed in rrvsh/pi-claude-bridge-nix.
  • Focused tests now run in Nix checkPhase:
    • tests/unit-import.mjs
    • tests/unit-querycontext.mjs
    • tests/unit-rate-limit.mjs
    • tests/unit-tool-result-delivery.mjs
  • GitHub Actions Build runs passed for Linux and Darwin, including Cachix publication:
    • 28343974050
    • 28344504492

Downstream/tools validation:

  • nix develop -c just check-nix passed.
  • auto fetched the final aarch64-darwin bridge package for 332eb2b from https://rrvsh.cachix.org.

Live runtime validation on auto:

  • Ran Pi with the GitHub/Cachix bridge package directly, bypassing any local package ambiguity.
  • Used CLAUDE_BRIDGE_STREAM_IDLE_TIMEOUT=5s to make the old failure easy to trigger.
  • Prompt forced two separate Bash calls:
    • sleep 1; printf 'DEFER_REPRO_A\n'
    • sleep 8; printf 'DEFER_REPRO_B\n'
  • Output was exactly:
DEFER_REPRO_A
DEFER_REPRO_B
  • Debug log showed the formerly missing second tool call was queued, flushed, executed, and resolved:
processAssistantMessage boundary: queued tool_use [...]
mcp handler: bash [...] → waiting
provider: flushing 1 deferred tool calls to Pi [...]
provider: resolving bash [...]
  • No tool_result_delivery_mismatch diagnostic was produced during the passing run.

Caveats

  • This PR updates the host configuration to consume the packaged bridge. The bridge implementation itself lives in rrvsh/pi-claude-bridge-nix.
  • The live test used GUI-launched Terminal on auto because noninteractive SSH did not have Claude Code login state available.
  • There is an unrelated post-run debug line about stale extension context during delayed session persistence in a no-session test run. It did not affect the tool-call test outcome and is separate from the delivery/idle-timeout fixes.

@rrvsh rrvsh merged commit 3394b46 into prime Jun 29, 2026
4 checks passed
@rrvsh rrvsh deleted the pi-claude-bridge-nix-integration branch June 29, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant